package com.tca.common.data.mybatis.interceptor;

import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.tca.common.data.mybatis.annotation.EncryptEnabled;
import com.tca.common.data.mybatis.annotation.EncryptField;
import com.tca.common.data.mybatis.utils.EncryptFieldUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.*;

/**
 * 拦截写请求的插件。插件生效仅支持预编译的sql
 *
 * @author ;
 */
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
})
@Component
public class EncryptWriteInterceptor implements Interceptor {

    private static final String MAPPED_STATEMENT = "delegate.mappedStatement";
    private static final String BOUND_SQL = "delegate.boundSql";


    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue(MAPPED_STATEMENT);
        SqlCommandType commandType = mappedStatement.getSqlCommandType();

        BoundSql boundSql = (BoundSql) metaObject.getValue(BOUND_SQL);
        Object params = boundSql.getParameterObject();
        if (params instanceof Map) {
			handleMapParameters(mappedStatement.getConfiguration(), boundSql, (Map)params, commandType);
            return invocation.proceed();
        }
        EncryptEnabled sensitiveEncryptEnabled = params != null ? params.getClass().getAnnotation(EncryptEnabled.class) : null;
        if (sensitiveEncryptEnabled != null && sensitiveEncryptEnabled.value()) {
            handleParameters(mappedStatement.getConfiguration(), boundSql, params, commandType);
        }
        return invocation.proceed();
    }

    private void handleMapParameters(Configuration configuration, BoundSql boundSql, Map params, SqlCommandType commandType) throws Exception {
		Collection values = params.values();
		HashSet processedValues = new HashSet();
		for (Object value : values) {
			EncryptEnabled sensitiveEncryptEnabled = value != null ? value.getClass().getAnnotation(EncryptEnabled.class) : null;
			if (sensitiveEncryptEnabled != null && sensitiveEncryptEnabled.value() && !processedValues.contains(value)) {
				handleParameters(configuration, boundSql, value, commandType);
				// 处理过后放入以处理集合，防止重复加密
				processedValues.add(value);
			}
		}

	}

    private void handleParameters(Configuration configuration, BoundSql boundSql, Object param, SqlCommandType commandType) throws Exception {

        Map<String, Object> newValues = new HashMap<>(16);
        MetaObject metaObject = configuration.newMetaObject(param);

        for (Field field : param.getClass().getDeclaredFields()) {
            if (!metaObject.hasGetter(field.getName())) {
                continue;
            }

            Object value = metaObject.getValue(field.getName());
            Object newValue = value;
            if (value instanceof CharSequence) {
                newValue = handleEncryptField(field, newValue);
            }
            if (value != null && newValue != null && !value.equals(newValue)) {
                newValues.put(field.getName(), newValue);
            }

        }
        for (Map.Entry<String, Object> entry : newValues.entrySet()) {
            boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
			if (metaObject.hasSetter(entry.getKey())) {
				metaObject.setValue(entry.getKey(), entry.getValue());
			}
        }

    }

    private boolean isWriteCommand(SqlCommandType commandType) {
        return SqlCommandType.UPDATE.equals(commandType) || SqlCommandType.INSERT.equals(commandType);
    }

    private Object handleEncryptField(Field field, Object value) {

        EncryptField encryptField = field.getAnnotation(EncryptField.class);
        Object newValue = value;
        if (encryptField != null && value != null) {
            newValue = EncryptFieldUtils.encrypt(value.toString());
        }
        return newValue;
    }


    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    @Override
    public void setProperties(Properties properties) {
        //do nothing
    }
}
